1 module node_dlang;
2 
3 public import js_native_api_types : napi_env, napi_value, napi_callback;
4 import node_api;
5 import js_native_api;
6 
7 import std.conv : to, text;
8 import std.string : toStringz;
9 import std.traits;
10 import std.algorithm;
11 debug import std.stdio;
12 
13 import std.variant;
14 template isVariantN (alias T) {
15   enum isVariantN = __traits(isSame, TemplateOf!T, VariantN);
16 }
17 
18 auto napiIdentity (napi_env _1, napi_value value, napi_value * toRet) {
19   *toRet = value;
20   return napi_status.napi_ok;
21 }
22 
23 alias ExternD (T) = SetFunctionAttributes!(T, "D", functionAttributes!T);
24 alias ExternC (T) = SetFunctionAttributes!(T, "C", functionAttributes!T);
25 
26 auto toNapiValueArray (T ...)(napi_env env, T args) {
27   napi_value [args.length] argVals;
28   foreach (i, arg; args) {
29     // Create a temporary value with the casted data.
30     argVals [i] = arg.toNapiValue (env);
31   }
32   return argVals;
33 }
34 
35 auto constructor (RetType, T ...)(napi_env env, napi_value constructorNapi, T args) {
36   const asArr = toNapiValueArray (env, args);
37   napi_value toRet;
38   auto status = napi_new_instance (
39     env
40     , constructorNapi
41     , asArr.length
42     , asArr.ptr
43     , &toRet
44   );
45   assert (status == napi_status.napi_ok, `Error calling JS constructror`);
46   return fromNapi!RetType (env, toRet);
47 }
48 
49 // If the first argument to the delegate (in other words, R) is a napi_value
50 // that is used as the context.
51 // Otherwise the global context is used.
52 auto jsFunction (R)(napi_env env, napi_value func, R * toRet) {
53   alias Params = Parameters!R;
54   alias RetType = ReturnType!R;
55   // Cannot assign toRet here for extern(C) Rs or LDC complains :(
56   auto foo = delegate RetType (Params args) {
57     static if (args.length > 0 && is (Params [0] == napi_value)) {
58       // Use provided context.
59       auto status = napi_status.napi_generic_failure;
60       napi_value context = args [0];
61       enum firstArgPos = 1;
62     } else {
63       napi_value context;
64       auto status = napi_get_global (env, &context);
65       assert (status == napi_status.napi_ok);
66       enum firstArgPos = 0;
67     }
68     /+napi_value [args.length - firstArgPos] napiArgs;
69     
70     foreach (i, arg; args [firstArgPos..$]) {
71       napiArgs [i] = arg.toNapiValue (env);
72     }+/
73     auto napiArgs = toNapiValueArray (env, args [firstArgPos .. $]);
74     napi_value returned;
75     status = napi_call_function (
76       env
77       , context
78       , func
79       , napiArgs.length
80       , napiArgs.ptr
81       , &returned
82     );
83     if (status != napi_status.napi_ok) {
84       debug stderr.writeln (`Got status `, status);
85       throw new Exception (`JS call errored :(`);
86     }
87     
88     static if (!is (RetType == void)) {
89       return fromNapi!RetType (env, returned);
90     }
91   };
92   *toRet = cast (R) foo;
93   return napi_status.napi_ok;
94 }
95 
96 auto getDgPointer (FP)(napi_env env, napi_value func, FP * toRet) {
97   static if (is (FP == F*, F)) {
98     import std.conv : emplace;
99     char [F.sizeof] buf;
100     /+
101     auto c = emplace!C(buf, 5);
102     auto intermediatePtr = new F [1].ptr;
103     jsFunction (env, func, intermediatePtr);
104     *toRet = intermediatePtr;
105     +/
106   } else static assert (0);
107   return napi_status.napi_ok;
108 }
109 
110 auto reference (napi_env env, ref napi_value obj) {
111   napi_ref toRet;
112   auto status = napi_create_reference (env, obj, 1, &toRet);
113   if (status != napi_status.napi_ok) throw new Exception (
114     text (`Reference creation failed: `, status)
115   );
116   return toRet;
117 }
118 
119 // Note this might escape values out of scope
120 auto val (napi_env env, napi_ref reference) {
121   napi_value toRet;
122   auto status = napi_get_reference_value (env, reference, &toRet);
123   if (status != napi_status.napi_ok) throw new Exception (`Could not get value from reference`);
124   assert (toRet != null);
125   return toRet;
126 }
127 
128 class JSException : Exception {
129   napi_value jsException;
130   this (
131     napi_value jsException
132     , string message = `JS Exception`
133     , string file = __FILE__
134     , size_t line = __LINE__
135     , Throwable nextInChain = null) {
136     this.jsException = jsException;
137     super (message, file, line, nextInChain);
138   }
139 }
140 
141 auto callNapi (Args ...)(napi_env env, napi_value context, napi_value func, Args args) {
142   napi_value [args.length] napiArgs;
143   static foreach (i, arg; args) {
144     napiArgs [i] = arg.toNapiValue (env);
145   }
146   napi_value returned;
147   auto status = napi_call_function (env, context, func, args.length, napiArgs.ptr, &returned);
148   if (status == napi_status.napi_pending_exception) {
149     napi_value exceptionData;
150     debug stderr.writeln (`Got JS exception`);
151     napi_get_and_clear_last_exception (env, &exceptionData);
152     throw new JSException (exceptionData);
153   } else if (status != napi_status.napi_ok) {
154     debug stderr.writeln (`Got JS status `, status);
155     throw new Exception (text (`Call errored for args `, args));
156   }
157   return returned;
158 }
159 
160 // Get a property.
161 auto p (RetType = napi_value, S) (napi_value obj, napi_env env, S propName) 
162 if (isSomeString!S) {
163   napi_value toRet;
164   auto key = propName.toNapiValue (env);
165   auto status = napi_get_property (env, obj, key, &toRet);
166   if (status != napi_status.napi_ok) {
167     throw new Exception (`Failed to get property ` ~ propName);
168   }
169   return fromNapi!RetType (env, toRet);
170 }
171 // Assign a property.
172 void p (InType, S) (napi_value obj, napi_env env, S propName, InType newVal)
173 if (isSomeString!S) {
174   auto key = propName.toNapiValue (env);
175   auto status = napi_set_property (env, obj, key, newVal.toNapiValue (env));
176   if (status != napi_status.napi_ok) {
177     throw new Exception (`Failed to set property ` ~ propName.to!string);
178   }
179 }
180 
181 private {
182   struct NapiRefWithId { napi_ref v; }
183   struct NapiValWithId { napi_value v; }
184 }
185 
186 struct JSVar {
187   napi_env env;
188   // DMD seems to handle well the difference between napi_ref and napi_value
189   // but LDC doesn't, so wrapped the types.
190   alias CtxRefT = Algebraic! (NapiRefWithId, NapiValWithId);
191   CtxRefT ctxRef;
192   this (napi_env env, napi_value nVal) {
193     assert (env != null && nVal != null);
194     this.env = env;
195     napi_valuetype napiType;
196     assert (napi_typeof(env, nVal, &napiType) == napi_status.napi_ok);
197     if (napiType == napi_valuetype.napi_object) {
198       this.ctxRef = NapiRefWithId (reference (env, nVal));
199     } else {
200       // TODO: Check cases such as functions which might be problematic.
201       this.ctxRef = NapiValWithId (nVal);
202     }
203   }
204   this (T) (napi_env env, T val) {
205     this (env, val.toNapiValue (env));
206   }
207 
208   auto constructor (RetType = JSVar, T ...)(T args) {
209     return .constructor!RetType (this.env, this.context (), args);
210   }
211 
212   auto context () {
213     return ctxRef.visit! (
214       (NapiValWithId v) => v.v
215       , (NapiRefWithId r) => val (env, r.v)
216     );
217   }
218 
219   auto toNapiValue (napi_env env) {
220     assert (this.context () != null);
221     return this.context ();
222   }
223 
224   auto opIndex (S)(S propName) if (isSomeString!S) {
225     return context.p!JSVar (env, propName);
226   }
227 
228   auto opIndexAssign (T, S)(T toAssign, S propName) if (isSomeString!S) {
229     context ().p (env, propName, toAssign);
230   }
231 
232   template opDispatch (string s) {
233     R opDispatch (R = JSVar, T...)(T args) {
234       auto ctx = this.context ();
235       auto toCallAsNapi = ctx.p (env, s);
236       auto asCallable = fromNapi! (R delegate (napi_value, T))(env, toCallAsNapi);
237       static if (is (R == void)) {
238         asCallable (ctx, args);
239       } else {
240         return asCallable (ctx, args);
241       }
242     }
243   }
244 
245   // Convenience console.log function
246   auto jsLog () {
247     console (this.env).log (this.context ());
248   }
249 
250   bool isUndefined () {
251     return .isUndefined (this.env, this.context ());
252   }
253 
254   auto opCast (T) () {
255     return fromNapi!T (env, this.context ());
256   }
257 
258   auto opCall (R = JSVar, T ...) (T args) {
259     auto ctx = this.context ();
260     auto toCall = fromNapi! (R delegate (napi_value, T)) (env, ctx);
261     return toCall (ctx, args);
262   }
263 }
264 
265 struct Promise {
266   @disable this ();
267   napi_deferred deferred;
268   JSVar promise;
269   napi_env env;
270   this (napi_env env) {
271     this.env = env;
272     napi_value napiPromise;
273     auto status = napi_create_promise (env, &deferred, &napiPromise);
274     promise = JSVar (env, napiPromise);
275     assert (status == napi_status.napi_ok);
276   }
277   void resolve (T)(T toRet) {
278     auto status = napi_resolve_deferred (env, deferred, toRet.toNapiValue (env));
279     assert (status == napi_status.napi_ok);
280   }
281   void reject (T)(T toRet) {
282     auto status = napi_reject_deferred (env, deferred, toRet.toNapiValue (env));
283     assert (status == napi_status.napi_ok);
284   }
285   napi_value toNapiValue (napi_env env) {
286     assert (env == this.env);
287     return this.promise.context ();
288   }
289 }
290 
291 /// Similar to JSObj but doesn't have a reference counter, so cannot be used
292 /// after the JS call that has the scope where this was created.
293 /// Template is a struct type that contains fields and function declarations
294 /// that this struct will attempt to copy in signature but with JS type conversions.
295 /// Do note that accessing members is done lazily.
296 alias ScopedJSObj (Template) = JSObj!(Template, false);
297 
298 struct Named { string name; };
299 /// Stores a JS value with a reference counter so that JS's GC doesn't collect
300 /// it whilst it's stored in D.
301 /// Template is a struct type that contains fields and function declarations
302 /// that this struct will attempt to copy in signature but with JS type conversions.
303 /// Do note that accessing members is done lazily.
304 struct JSObj (Template, bool useRefCount = true) {
305   /// Convenience console.log (this) function
306   void jsLog () {
307     console (this.env).log (this.context);
308   }
309 
310   void toString (scope void delegate (const (char)[]) sink) {
311     if (env is null || this.context is null) {
312       sink (`null JSObj`);
313     } else {
314       // Note: this could generate an error 
315       napi_value asStr;
316       napi_coerce_to_string (env, this.context, &asStr);
317       sink (fromNapi!string (env, asStr));
318     }
319   }
320 
321   alias FieldNames = __traits (allMembers, Template);
322   private template nameForCaller (string name) {
323     alias nameUDAs = getUDAs! (mixin (`Template.` ~ name), Named);
324     static if (nameUDAs.length == 0) {
325       alias nameForCaller = name;
326     } else {
327       static assert (nameUDAs.length == 1, `Cannot have more than one Named UDA`);
328       enum nameForCaller = nameUDAs [0].name;
329     }
330   }
331   import std.meta;
332   // Equals to FieldNames unless the @named uda is used.
333   alias NamesForCaller = staticMap! (nameForCaller, FieldNames);
334   private template type (string name) {
335     alias type = typeof (mixin (`Template.` ~ name));
336   }
337   alias FieldTypes = staticMap! (type, FieldNames);
338   private static auto positions () {
339     size_t [] funPositions;
340     size_t [] fieldPositions;
341     static foreach (i, Member; FieldNames) {
342       // Ignore opAssign, which might be implicitly generated
343       static if (Member != `opAssign` && !Member.startsWith (`__`)) {
344         static if (mixin (`isFunction! (Template.` ~ Member ~ `)`)) {
345           funPositions ~= i;
346         } else {
347           fieldPositions ~= i;
348         }
349       }
350     }
351     import std.typecons : tuple;
352     return tuple!(`funPositions`, `fieldPositions`) (funPositions, fieldPositions);
353   }
354 
355   static assert (NamesForCaller.length == FieldTypes.length);
356   // Useful for type checking. 
357   enum dlangNodeIsJSObj = true;
358 
359   private enum Positions = positions ();
360   private enum FunPositions = Positions.funPositions;
361   private enum FieldPositions = Positions.fieldPositions;
362 
363   napi_env env;
364   static if (useRefCount) {
365     napi_ref ctxRef = null;
366     auto context () {
367       return val (env, this.ctxRef);
368     }
369   } else {
370     napi_value context;
371   }
372 
373   private enum typeMsg = `JSObj template args should be pairs of strings with types`;
374   // Creating a new object from D.
375   this (napi_env env) {
376     this.env = env;
377     static if (useRefCount) {
378       auto context = napi_value ();
379       auto status = napi_create_object (env, &context);
380       assert (status == napi_status.napi_ok);
381       ctxRef = reference (env, context);
382     }
383   }
384 
385   // Assigning from a JS object.
386   this (napi_env env, napi_value context) {
387     this.env = env;
388     // Keep alive. Note this will NEVER be GC'ed
389     static if (useRefCount) {
390       ctxRef = reference (env, context);
391     } else {
392       this.context = context;
393     }
394   }
395   
396   // Copy ctor.
397   this (ref return scope typeof (this) rhs) {
398     this.env = rhs.env;
399     static if (useRefCount) {
400       this.ctxRef = rhs.ctxRef;
401       if (ctxRef != null) {
402         auto status = napi_reference_ref (env, ctxRef, null);
403         //writeln (`> Ref count is `, currentRefCount);
404         assert (status == napi_status.napi_ok);
405       }
406     } else {
407       this.context = rhs.context;
408     }
409   }
410   ~this () {
411     // writeln ("Destructing JSobj " ~ exportsAlias.stringof);
412     // uint currentRefCount;
413     static if (useRefCount) {
414       if (ctxRef != null) {
415         auto status = napi_reference_unref (env, ctxRef, null);
416         assert (
417           status == napi_status.napi_ok
418           , `Got status when doing unref ` ~ status.to!string
419         );
420         //writeln (`< Ref count is `, currentRefCount);
421       }
422     }
423   }
424 
425   static foreach (i, FunPosition; FunPositions) {
426     // Functions called constructor are equivalent to using new in JS.
427     static if (FieldNames [FunPosition] == `constructor`) {
428       pragma (msg, `Found constructor`);
429       static assert (
430         is (ReturnType! (FieldTypes [FunPosition]) == void)
431         , `Please put void return type on JS constructor declarations, found `
432           ~ ReturnType! (FieldTypes [FunPosition]).stringof
433       );
434       auto constructor (Parameters! (FieldTypes [FunPosition]) args) {
435         return .constructor!(typeof (this)) (this.env, this.context (), args);
436       }
437     } else {
438       // Add function that simply uses callNapi.
439       mixin (
440         q{auto } ~ NamesForCaller [FunPosition] ~ q{ (Parameters! (FieldTypes [FunPosition]) args) {
441           alias FunType = FieldTypes [FunPosition];
442           alias RetType = ReturnType!(FunType);
443             //auto context = val (env, this.ctxRef);
444             auto toCall = context
445               .p! (RetType delegate (napi_value, Parameters!FunType))
446                 (env, NamesForCaller [FunPosition]);
447             static if (is (RetType == void)) {
448               toCall (context, args);
449             } else {
450               return toCall (context, args);
451             }
452           }
453         }
454       );
455     }
456   }
457   static foreach (i, FieldPosition; FieldPositions) {
458     // Setter.
459     static if (isVariantN! (FieldTypes [FieldPosition])) {
460       // Also add implicit conversions :)
461       static foreach (PossibleType; TemplateArgsOf!(FieldTypes [FieldPosition])[1..$]) {
462         mixin (q{
463           void } ~ NamesForCaller [FieldPosition] ~ q{ (PossibleType toSet) {
464             enum fieldName = NamesForCaller [FieldPosition];
465             auto asNapi = toSet.toNapiValue (env);
466             auto propName = fieldName.toStringz;
467             //auto context = val (env, this.ctxRef);
468             auto status = napi_set_named_property (env, context, propName, asNapi);
469             assert (status == napi_status.napi_ok, `Couldn't set property ` ~ fieldName);
470           }
471         });
472       };
473     }
474     mixin (q{
475       void } ~ FieldNames [FieldPosition] ~ q{ (FieldTypes [FieldPosition] toSet) {
476         enum fieldName = FieldNames [FieldPosition];
477         auto asNapi = toSet.toNapiValue (env);
478         auto propName = fieldName.toStringz;
479         //auto context = val (env, this.ctxRef);
480         auto status = napi_set_named_property (env, context, propName, asNapi);
481         assert (status == napi_status.napi_ok, `Couldn't set property ` ~ fieldName);
482       }
483     });
484     // Getter.
485     // Pretty similar in both cases, would like to deduplicate it but
486     // static foreach is fun.
487     static if (isVariantN! (FieldTypes [FieldPosition])) {
488       // Getting Algebraics/VariantNs must specify the returned type.
489       mixin (q{
490         auto } ~ FieldNames [FieldPosition] ~ q{ (Type) () {
491           return fromNapi!Type (
492             env
493             , context.p (env, FieldNames [FieldPosition])
494           );
495         }
496       });
497     } else {
498       static if (isFunctionPointer! (FieldTypes [FieldPosition])) {
499         // Function pointers must become delegates because jsFunction
500         // adds a context.
501         // They also have a different signature so that two sets of parens
502         // aren't needed to call them.
503         mixin (q{auto } ~ FieldNames [FieldPosition] ~ q{ (Parameters!(FieldTypes [FieldPosition]) args) {
504           import std.functional : toDelegate;
505           alias RetType = typeof (FieldTypes [FieldPosition].init.toDelegate);
506           return fromNapi!RetType (
507             env
508             , context.p (env, FieldNames [FieldPosition])
509           ) (args);
510         }});
511       } else {
512         // Other types use a direct getter function.
513         mixin (q{auto } ~ FieldNames [FieldPosition] ~ q{ () {
514           return fromNapi! (FieldTypes [FieldPosition]) (
515             env
516             , context.p (env, FieldNames [FieldPosition])
517           );
518         }});
519       }
520     }
521   }
522 }
523 
524 // Convenience console type.
525 private struct Console_ {
526   void log (napi_value);
527 };
528 alias Console = JSObj!Console_;
529 auto console = (napi_env env) => global!Console (env, `console`);
530 void jsLog (T)(napi_env env, T toLog) {
531   console (env).log (toLog.toNapiValue (env));
532 }
533 auto global (napi_env env) {
534   napi_value val;
535   auto status = napi_get_global (env, &val);
536   assert (status == napi_status.napi_ok, `Couldn't get global context`);
537   return val;
538 }
539 
540 auto global (RetType = JSVar)(napi_env env, string name) {
541   return fromNapi!RetType (env, global (env).p (env, name));
542 }
543 
544 auto getJSobj (T)(napi_env env, napi_value ctx, T * toRet) {
545   assert (toRet != null);
546   *toRet = T (env, ctx);
547   return napi_status.napi_ok;
548 }
549 
550 auto getStr (StrType)(napi_env env, napi_value napiVal, StrType * toRet) {
551   // Try with statically allocated buffer for small strings
552   assert (toRet != null);
553   size_t readChars = 0;
554   static if (is (StrType == string)) {
555     alias CharType = char;
556     char [2048] inBuffer;
557     alias conversionFunction = napi_get_value_string_utf8;
558   } else static if (is (StrType == wstring)) {
559     alias CharType = wchar;
560     ushort [2048] inBuffer;
561     alias conversionFunction = napi_get_value_string_utf16;
562   } else static assert (
563     false
564     , `N-API doesn't provide conversion for UTF-32 (dstrings), use string or wstring instead`
565   );
566   auto status = conversionFunction (
567     env
568     , napiVal
569     , inBuffer.ptr
570     , inBuffer.length
571     , & readChars
572   );
573   if (status != napi_status.napi_ok) {
574     return status;
575   }
576   
577   if (readChars == inBuffer.length - 1) {
578     // String bigger than buffer :( need a dynamic array.
579     // Technically this is not needed for the specific case of
580     // exactly size inBuffer.length - 1
581     // Get string size to allocate an array.
582     status = conversionFunction (
583       env
584       , napiVal
585       , null
586       , 0
587       , & readChars
588     );
589     assert (status == napi_status.napi_ok);
590     // Try again with bigger size.
591     auto buffer = new typeof(inBuffer [0]) [readChars + 1]; // include null terminator
592     status = conversionFunction (
593       env
594       , napiVal
595       , buffer.ptr
596       , buffer.length
597       , & readChars
598     );
599     assert (status == napi_status.napi_ok);
600     *toRet = buffer.ptr [0..readChars].map!(to!CharType).to!StrType;
601   } else {
602     // Got it on first try
603     *toRet = inBuffer.ptr [0..readChars].map!(to!CharType).to!StrType;
604   }
605   return napi_status.napi_ok;
606 }
607 
608 auto getFloat (napi_env env, napi_value napiVal, float * toRet) {
609   double intermediate;
610   auto status = napi_get_value_double (env, napiVal, &intermediate);
611   *toRet = intermediate.to!float;
612   return status;
613 }
614 
615 void inJSScope (alias fun)(napi_env env) {
616   napi_handle_scope jsScope;
617   auto status = napi_open_handle_scope (env, &jsScope);
618   assert (status == napi_status.napi_ok);
619   scope (exit) napi_close_handle_scope (env, jsScope);
620   fun ();
621 }
622 
623 auto getAA (V)(napi_env env, napi_value napiVal, V [string] * toRet) {
624   * toRet = V [string].init;
625   napi_value propertyNames;
626   auto status = napi_get_property_names (env, napiVal, &propertyNames);
627   assert (status == napi_status.napi_ok);
628   uint propertyNamesLength;
629   status = napi_get_array_length (env, propertyNames, &propertyNamesLength);
630   assert (status == napi_status.napi_ok);
631   foreach (i; 0 .. propertyNamesLength) {
632     inJSScope! (() {
633       napi_value keyNapi;
634       status = napi_get_element (env, propertyNames, i, &keyNapi);
635       assert (status == napi_status.napi_ok);
636       napi_value element;
637       status = napi_get_property (env, napiVal, keyNapi, &element);
638       assert (status == napi_status.napi_ok);
639       (* toRet) [fromNapi!string (env, keyNapi)] = fromNapi!V (env, element);
640     }) (env);
641   }
642   return napi_status.napi_ok;
643 }
644 
645 auto getJSVar (napi_env env, napi_value napiVal, JSVar * toRet) {
646   *toRet = JSVar (env, napiVal);
647   return napi_status.napi_ok;
648 }
649 
650 auto getStaticArray (A)(napi_env env, napi_value napiVal, A * toRet) {
651   // Copying could be avoided
652   auto asDynamicArr = (* toRet) [];
653   auto toRetNapi = getArray (env, napiVal, & asDynamicArr);
654   foreach (i, val; asDynamicArr) {
655     (* toRet) [i] = val;
656   }
657   return toRetNapi;
658 }
659 
660 auto getArray (A)(napi_env env, napi_value napiVal, A [] * toRet) {
661   import std.array;
662   Appender!(A []) toRetAppender;
663   uint arrLength;
664   auto status = napi_get_array_length (env, napiVal, &arrLength);
665   assert (
666     status == napi_status.napi_ok
667     , `Error getting array length, maybe object isn't array or N-API errored on`
668       ~ ` the call before this one.`
669   );
670   foreach (i; 0 .. arrLength) {
671     inJSScope! (() {
672       napi_value toConvert;
673       status = napi_get_element (env, napiVal, i, &toConvert);
674       assert (status == napi_status.napi_ok, `Couldn't get element from array`);
675       toRetAppender ~= fromNapi!A (env, toConvert);
676     }) (env);
677   }
678   *toRet = toRetAppender.data;
679   return napi_status.napi_ok;
680 }
681 
682 auto getTypedArray (T)(napi_env env, napi_value napiVal, TypedArray!T * toRet) {
683   napi_typedarray_type type;
684   size_t length;
685   void * data;
686   napi_value arrayBuffer;
687   size_t offset;
688   auto status = napi_get_typedarray_info (
689     env
690     , napiVal
691     , & type
692     , & length
693     , & data
694     , & arrayBuffer
695     , & offset
696   );
697 
698   enum expectedType = TypedArray!T.type;
699   assert (
700     // ubyte accepts both clamped and unclamped arrays.
701     expectedType == type || (is (T == ubyte)
702       && type == napi_typedarray_type.napi_uint8_clamped_array)
703     , `TypedArray type doesn't match (make sure signedness is correct too)`
704   );
705   toRet.internal = (cast (T *) data) [0 .. length];
706   return status;
707 }
708 
709 auto getStruct (S)(napi_env env, napi_value napiVal, S * toRet) {
710   *toRet = S.init;
711   foreach (fieldName; FieldNameTuple!S) {
712     alias FieldType = typeof (__traits (getMember, *toRet, fieldName));
713     auto napiProp = napiVal.p (env, fieldName);
714     __traits (getMember, *toRet, fieldName) = fromNapi!FieldType (env, napiProp);
715   }
716   return napi_status.napi_ok;
717 }
718 
719 template fromNapiB (T) {
720   static if (is (T == bool)) {
721     alias fromNapiB = napi_get_value_bool;
722   } else static if (is (T == double)) {
723     alias fromNapiB = napi_get_value_double;
724   } else static if (is (T == int)) {
725     alias fromNapiB = napi_get_value_int32;
726   } else static if (is (T == uint)) {
727     alias fromNapiB = napi_get_value_uint32;
728   } else static if (is (T == long)) {
729     alias fromNapiB = napi_get_value_int64;
730   } else static if (is (T == ulong)) {
731     alias fromNapiB = napi_get_value_bigint_uint64;
732   } else static if (is (T == double)) {
733     alias fromNapiB = napi_get_value_double;
734   } else static if (is (T == float)) {
735     alias fromNapiB = getFloat;
736   } else static if (is (T == TypedArray!A, A)) {
737     alias fromNapiB = getTypedArray;
738   } else static if (is (T == V [string], V)) {
739     alias fromNapiB = getAA;
740   } else static if (isSomeString!T) {
741     alias fromNapiB = getStr;
742   } else static if (is (T == Nullable!A, A)) {
743     alias fromNapiB = getNullable!A;
744   } else static if (is (T == napi_value)) {
745     alias fromNapiB = napiIdentity;
746   } else static if (isDelegate!T) {
747     alias fromNapiB = jsFunction;
748   } else static if (is (T == JSVar)) {
749     alias fromNapiB = getJSVar;
750   } else static if (isStaticArray!T) {
751     alias fromNapiB = getStaticArray;
752   } else static if (is (T == A[], A)) {
753     alias fromNapiB = getArray;
754   } else static if (is (T == A*, A) && isDelegate!A) {
755     alias fromNapiB = getDgPointer;
756   } else static if (isFunctionPointer!T) {
757     // Need context to perform JS calls.
758     static assert (
759       0
760       , `Cannot receive function pointers, use delegates instead`
761     );
762   } else static if (__traits(hasMember, T, `dlangNodeIsJSObj`)) {
763     alias fromNapiB = getJSobj;
764   } else static if (isVariantN!T) {
765     static assert (
766       0
767       , `Don't use fromNapiB to get a VariantN/Algebraic, get the expected type instead`
768     );
769   } else static if (__traits(isPOD, T)) {
770     alias fromNapiB = getStruct;
771   } else {
772     static assert (0, `Not implemented: Conversion from JS type for ` ~ T.stringof);
773   }
774 }
775 
776 napi_status getNullable (BaseType) (
777   napi_env env
778   , napi_value value
779   , Nullable!BaseType * toRet
780 ) {
781   auto erroredToJS = () => napi_throw_error (
782     env, null, `Failed to parse to ` ~ Nullable!BaseType.stringof
783   );
784   BaseType toRetNonNull;
785   auto status = fromNapiB!BaseType (env, value, &toRetNonNull);
786   if (status != napi_status.napi_ok) {
787     *toRet = Nullable!BaseType ();
788   } else {
789     *toRet = Nullable!BaseType (toRetNonNull);
790   }
791   return napi_status.napi_ok;
792 }
793 
794 import std.typecons : Nullable;
795 /// Gets a D typed value from a napi_value
796 T fromNapi (T, string argName = ``)(napi_env env, napi_value value) {
797   T toRet;
798   auto erroredToJS = () => napi_throw_error (
799     env, null, `Failed to parse ` ~ argName ~ ` to ` ~ T.stringof
800   );
801   try {
802     auto status = fromNapiB!T (env, value, &toRet);
803     if (status != napi_status.napi_ok) {
804       erroredToJS ();
805     }
806   } catch (Exception ex) {
807     erroredToJS ();
808   }
809   return toRet;
810 }
811 
812 void throwInJS (napi_env env, string message) {
813   napi_throw_error (env, null, message.toStringz);
814 }
815 
816 napi_status boolToNapi (napi_env env, bool toConvert, napi_value * toRet) {
817   return napi_get_boolean (env, toConvert, toRet);
818 }
819 
820 // O(n) operation. Use BufferArrays to avoid element-by-element JS object 
821 // creation.
822 napi_status arrayToNapi (F)(napi_env env, F[] array, napi_value * toRet) {
823   assert (toRet != null);
824   auto status = napi_create_array_with_length (env, array.length, toRet);
825   assert (status == napi_status.napi_ok);
826   foreach (i, val; array) {
827     inJSScope! (() {
828       auto nv = val.toNapiValue (env);
829       status = napi_set_element (env, *toRet, i.to!uint, nv);
830       assert (status == napi_status.napi_ok);
831     }) (env);
832   }
833   return status;
834 }
835 
836 /// Note: No conversion implemented for Uint8ClampedArray.
837 struct TypedArray (Element) {
838   /// Constructor that uses the provided internal array.
839   this (Element [] internal) {
840     this.internal = internal;
841   }
842   /// Constructor that allocates on JS mem.
843   this (napi_env env, uint length) {
844     auto buffer = global (env, `Uint8Array`).constructor (length);
845     this = cast (TypedArray!Element) buffer;
846   }
847   Element [] internal;
848   alias internal this;
849   static if (is (Element == byte)) {
850     enum type = napi_typedarray_type.napi_int8_array;
851   } else static if (is (Element == ubyte)) {
852     enum type = napi_typedarray_type.napi_uint8_array;
853   } else static if (is (Element == short)) {
854     enum type = napi_typedarray_type.napi_int16_array;
855   } else static if (is (Element == ushort)) {
856     enum type = napi_typedarray_type.napi_uint16_array;
857   } else static if (is (Element == int)) {
858     enum type = napi_typedarray_type.napi_int32_array;
859   } else static if (is (Element == uint)) {
860     enum type = napi_typedarray_type.napi_uint32_array;
861   } else static if (is (Element == float)) {
862     enum type = napi_typedarray_type.napi_float32_array;
863   } else static if (is (Element == double)) {
864     enum type = napi_typedarray_type.napi_float64_array;
865   } else static if (is (Element == long)) {
866     enum type = napi_typedarray_type.napi_bigint64_array;
867   } else static if (is (Element == ulong)) {
868     enum type = napi_typedarray_type.napi_biguint64_array;
869   } else {
870     static assert (0, `Cannot make TypedArray of ` ~ Element.stringof);
871   }
872 }
873 
874 private size_t tArrLastId = 0;
875 
876 import core.memory : GC;
877 private extern (C) void onTypedArrayFinalize (
878   napi_env env
879   , void * finalizeData
880   , void * hint
881 ) {
882   GC.removeRoot (finalizeData);
883 }
884 
885 /// Note: The array data must be kept alive in D.
886 napi_status typedArrayToNapi (T)(
887   napi_env env
888   , ref TypedArray!T array
889   , napi_value * toRet
890 ) {
891   napi_value arrayBuffer;
892   napi_finalize finalizeCb;
893   auto arrPtr = array.internal.ptr;
894   // Keep it alive on D side;
895   GC.addRoot (arrPtr);
896   // Also ensure that a moving collector does not relocate the object.
897   GC.setAttr (arrPtr, GC.BlkAttr.NO_MOVE);
898 
899   auto status = napi_create_external_arraybuffer (
900     env
901     , arrPtr
902     , T.sizeof * array.internal.length
903     , & onTypedArrayFinalize
904     , null
905     , & arrayBuffer
906   );
907   assert (status == napi_status.napi_ok);
908 
909   return napi_create_typedarray (
910     env
911     , TypedArray!T.type
912     , array.internal.length
913     , arrayBuffer
914     , 0
915     , toRet
916   );
917 }
918 
919 napi_status aaToNapi (V)(napi_env env, V [string] toConvert, napi_value * toRet) {
920   assert (toRet != null);
921   auto status = napi_create_object (env, toRet);
922   assert (status == napi_status.napi_ok);
923   foreach (key, value; toConvert) {
924     inJSScope! (() {
925       status = napi_set_named_property (env, *toRet, key.toStringz, value.toNapiValue (env));
926       assert (status == napi_status.napi_ok);
927     }) (env);
928   }
929   return napi_status.napi_ok;
930 }
931 
932 napi_status stringToNapi (StrType)(napi_env env, StrType toConvert, napi_value * toRet) {
933   assert (toRet != null);
934   static if (is (StrType == string)){
935     alias NapiCharType = char;
936     alias conversionFunction = napi_create_string_utf8;
937   } else static if (is (StrType == wstring)) {
938     alias NapiCharType = ushort;
939     alias conversionFunction = napi_create_string_utf16;
940   } else static assert (
941     false
942     , `Cannot convert UTF32 strings (dstrings) to JS. Please use strings or wstrings instead`
943   );
944   return conversionFunction (
945     env
946     , cast (const NapiCharType *) toConvert.ptr
947     , toConvert.length
948     , toRet
949   );
950 }
951 
952 napi_status nullableToNapi (T) (napi_env env, Nullable!T toConvert, napi_value * toRet) {
953   assert (toRet != null);
954   if (toConvert.isNull ()) {
955     return napi_get_null (env, toRet);
956   } else {
957     return toNapi!T (env, toConvert.get (), toRet);
958   }
959 }
960 
961 napi_status callbackToNapi (F)(
962   napi_env env
963   , napi_callback toConvert
964   , napi_value * toRet
965   , F * fPointer
966 ) {
967   assert (toRet != null);
968   return napi_create_function (env, null, 0, toConvert, fPointer, toRet);
969 }
970 
971 napi_status delegateToNapi (Dg)(napi_env env, Dg * toCall, napi_value * toRet) {
972   assert (toRet != null);
973   static assert (isDelegate!(Dg));
974   return callbackToNapi (env, &fromJsPtr! (Dg), toRet, toCall);
975 }
976 
977 napi_status callableToNapi (F)(napi_env env, F toCall, napi_value * toRet) {
978   assert (toRet != null);
979   static assert (!isDelegate!(F), `Use delegateToNapi instead`);
980   return callbackToNapi (env, &fromJsPtr!F, toRet, toCall);
981 }
982 
983 napi_status jsObjToNapi (T)(napi_env env, T toConvert, napi_value * toRet) {
984   assert (toRet != null);
985   assert (env == toConvert.env);
986   *toRet =  toConvert.context;
987   return napi_status.napi_ok;
988 }
989 
990 napi_status algebraicToNapi (T ...)(napi_env env, VariantN!T toConvert, napi_value * toRet) {
991   static assert (T.length > 1);
992   assert (toRet != null);
993   foreach (possibleType; T [1..$]) {
994     auto valueTried = toConvert.peek!possibleType;
995     if (valueTried != null) {
996       *toRet = toNapiValue (*valueTried, env);
997       return napi_status.napi_ok;
998     }
999   }
1000   assert (0, `Could not get value from Algebraic/VariantN`);
1001 }
1002 
1003 napi_status jsVarToNapi (napi_env env, JSVar toConvert, napi_value * toRet) {
1004   assert (toRet != null);
1005   assert (env == toConvert.env, `JS environments don't match`);
1006   *toRet = toConvert.context ();
1007   return napi_status.napi_ok;
1008 }
1009 
1010 napi_status tupleToNapi (T)(napi_env env, const auto ref T toConvert, napi_value * toRet) {
1011   assert (toRet != null);
1012   napi_create_object (env, toRet);
1013   foreach (i, field; toConvert.expand) {
1014     (*toRet).p (env, toConvert.fieldNames [i], field);
1015   }
1016   return napi_status.napi_ok;
1017 }
1018 
1019 napi_status structToNapi (S)(napi_env env, const auto ref S toConvert, napi_value * toRet) {
1020   assert (toRet != null);
1021   napi_create_object (env, toRet);
1022   foreach (fieldName; FieldNameTuple!S) {
1023     (*toRet).p (env, fieldName, __traits (getMember, toConvert, fieldName));
1024   }
1025   return napi_status.napi_ok;
1026 }
1027 
1028 template toNapi (alias T) {
1029   import std.typecons : Tuple;
1030   static if (is (T == bool)) {
1031     alias toNapi = boolToNapi;
1032   } static if (is (T == double)) {
1033     alias toNapi = napi_create_double;
1034   } else static if (is (T == int)) {
1035     alias toNapi = napi_create_int32;
1036   } else static if (is (T == uint)) {
1037     alias toNapi = napi_create_uint32;
1038   } else static if (is (T == long)) {
1039     alias toNapi = napi_create_int64;
1040   } else static if (is (T == ulong)) {
1041     alias toNapi = napi_create_bigint_uint64;
1042   } else static if (is (T : double)) {
1043     alias toNapi = napi_create_double;
1044   } else static if (is (T == V [string], V)) {
1045     alias toNapi = aaToNapi;
1046   } else static if (isSomeString!T) {
1047     alias toNapi = stringToNapi;
1048   } else static if (is (T == Nullable!A, A)) {
1049     alias toNapi = nullableToNapi!A;
1050   } else static if (is (T == napi_value)) {
1051     alias toNapi = napiIdentity;
1052   } else static if (is (T == Dg*, Dg)) {
1053     static if (isDelegate!Dg) {
1054       alias toNapi = delegateToNapi;
1055     } else static if (isFunctionPointer!T){
1056       alias toNapi = callableToNapi;
1057     } else {
1058       static assert (0, `Not implemented: Conversion to JS type for ` ~ T.stringof);
1059     }
1060   } else static if (isDelegate!T) {
1061     static assert (
1062       0
1063       , `Delegates must be sent as pointers to the delegate because of memory management`
1064     );
1065   } else static if (is (ExternC!T == napi_callback)) {
1066     static if (!is (T == ExternC!T)) {
1067       static assert (0, `Please use extern (C) for napi callbacks`);
1068     }
1069     alias toNapi = callbackToNapi;
1070   } else static if (is (T == TypedArray!A, A)) {
1071     alias toNapi = typedArrayToNapi;
1072   } else static if (isStaticArray!T) {
1073     alias toNapi = arrayToNapi;
1074   } else static if (is (T == A[], A)) {
1075     alias toNapi = arrayToNapi;
1076   } else static if (__traits(hasMember, T, `dlangNodeIsJSObj`)) {
1077     alias toNapi = jsObjToNapi;
1078   } else static if (isVariantN!T) {
1079     alias toNapi = algebraicToNapi;
1080   } else static if (__traits (isSame, TemplateOf!T, Tuple)) {
1081     alias toNapi = tupleToNapi;
1082   } else static if (__traits (isPOD, T)) {
1083     alias toNapi = structToNapi;
1084   } else {
1085     static assert (0, `Not implemented: Conversion to JS type for ` ~ T.stringof);
1086   }
1087 }
1088 
1089 napi_value toNapiValue (F)(
1090   F toCast, napi_env env
1091 ) {
1092   napi_value toRet;
1093   auto status = toNapi!F (env, toCast, &toRet);
1094   if (status != napi_status.napi_ok) {
1095     env.throwInJS (`Unable to create JS value for: ` ~ toCast.to!string);
1096   }
1097   return toRet;
1098 }
1099 
1100 napi_value undefined (napi_env env) {
1101   napi_value toRet;
1102   if (napi_get_undefined (env, &toRet) != napi_status.napi_ok) {
1103     env.throwInJS (`Unable to return void (undefined in JS)`);
1104   }
1105   return toRet;
1106 }
1107 
1108 bool isUndefined (napi_env env, napi_value val) {
1109   napi_valuetype valueType;
1110   assert (napi_typeof (env, val, &valueType) == napi_status.napi_ok);
1111   return valueType == napi_valuetype.napi_undefined;
1112 }
1113 
1114 private auto convertNapiSignature (F, alias toFinish)(
1115   napi_env env
1116   , napi_callback_info info
1117   , void ** delegateDataPtr = null
1118 ) {
1119   alias FunParams = Parameters!F;
1120   static if (FunParams.length > 0 && is (FunParams [0] == napi_env)) {
1121     immutable argCount = FunParams.length - 1;
1122     // First parameter is the env, which is from the 'env' param in withNapiExpectedSignature.
1123     // Thus it isn't stored as an napi_value.
1124     enum envParam = `env, `;
1125     enum firstValParam = 1;
1126   } else {
1127     immutable argCount = FunParams.length;
1128     enum envParam = ``;
1129     enum firstValParam = 0;
1130   }
1131   napi_value [argCount] argVals;
1132   size_t argCountMut = argCount;
1133   auto status = napi_get_cb_info (
1134     env
1135     , info
1136     , &argCountMut
1137     , argVals.ptr
1138     , null
1139     , delegateDataPtr
1140   );
1141   if (status != napi_status.napi_ok) {
1142     napi_throw_type_error (
1143       env
1144       , null
1145       , (`Failed to parse arguments for function ` ~ F.mangleof ~ `, incorrect amount?`)
1146         .toStringz
1147     );
1148   }
1149   static foreach (i, Param; FunParams [firstValParam .. $]) {
1150     // Create a temporary value with the casted data.
1151     mixin (`auto param` ~ i.to!string ~ ` = fromNapi!Param (env, argVals [i]);`);
1152   }
1153   // Now call Function with each of these casted values.
1154   import std.range;
1155   enum paramCalls = envParam ~ iota (argCount)
1156     .map!`"param" ~ a.to!string`
1157     .joiner (`,`)
1158     .to!string;
1159   static if (isDelegate!toFinish) {
1160     assert (delegateDataPtr != null);
1161     auto tmp = cast (F**) delegateDataPtr;
1162     toFinish = **tmp;
1163   }
1164   enum toMix = q{toFinish (} ~ paramCalls ~ q{)};
1165   enum retsVoid = Returns! (F, void);
1166   static if (retsVoid) {
1167     mixin (toMix ~ `;`);
1168     return undefined (env);
1169   } else {
1170     mixin (q{return } ~ toMix ~ q{.toNapiValue (env);});
1171   }
1172 }
1173 
1174 extern (C) napi_value fromJsPtr (F)(napi_env env, napi_callback_info info) {
1175   // It's a function pointer type, so must allocate it.
1176   F toCall;
1177   return convertNapiSignature! (F, toCall) (env, info, cast (void **) & toCall);
1178 }
1179 
1180 extern (C) napi_value withNapiExpectedSignature (alias Function)(
1181   napi_env env
1182   , napi_callback_info info
1183 ) {
1184   return convertNapiSignature!(typeof(Function), Function) (env, info, null);
1185 }
1186 
1187 template Returns (alias Function, OtherType) {
1188   enum Returns = is (ReturnType!Function == OtherType);
1189 }
1190 
1191 extern (C) alias void func (napi_env);
1192 template MainFunction (alias Function) {
1193   alias ToCall = Function;
1194   static assert (
1195     is (ExternC! (typeof (Function)) == func)
1196     , `MainFunction must be instantiated with a void function (napi_env)`
1197   );
1198 }
1199 
1200 bool isMainFunction (alias Function) () if (isCallable!Function) {
1201   return false;
1202 }
1203 import std.meta;
1204 bool isMainFunction (alias Function) () if (!isCallable!Function) {
1205   static if (__traits (compiles, TemplateOf!(Function))) {
1206     return __traits (isSame, TemplateOf!(Function), MainFunction);
1207   } else {
1208     return false;
1209   }
1210 }
1211 
1212 mixin template exportToJs (Exportables ...) {
1213   import node_api;
1214   import js_native_api;
1215   import std.string : toStringz;
1216   import std.traits;
1217 
1218   extern (C) napi_value exportToJs (napi_env env, napi_value exports) {
1219     import core.runtime;
1220     Runtime.initialize ();
1221     auto addExportable (alias Exportable)() {
1222       napi_status status;
1223       napi_value fn;
1224       status = napi_create_function (
1225         env
1226         , null
1227         , 0
1228         , & withNapiExpectedSignature!Exportable
1229         , null
1230         , &fn
1231       );
1232       if (status != napi_status.napi_ok) {
1233         napi_throw_error (env, null, "Was not able to wrap native function");
1234       } else {
1235         const fnName = Exportable.mangleof;
1236         status = napi_set_named_property (env, exports, fnName.toStringz, fn);
1237         if (status != napi_status.napi_ok) {
1238           napi_throw_error (
1239             env
1240             , null
1241             , ("Unable to populate exports for " ~ fnName).toStringz
1242           );
1243         }
1244       }
1245       return status;
1246     }
1247     static foreach (i, alias Exportable; Exportables) {
1248       static if (isCallable!Exportable) {
1249         if (addExportable!Exportable () != napi_status.napi_ok) {
1250           debug stderr.writeln (`Error registering function to JS`);
1251           return exports;
1252         } 
1253       } else static if (isMainFunction!Exportable ()) {
1254         // Just call it here (on module load).
1255         Exportable.ToCall (env);
1256       } else {
1257         const fieldName = (Exportables [i]).stringof.toStringz;
1258         // It's a field.
1259         // pragma (msg, `Got field ` ~ Exportable.stringof);
1260         auto toExp = Exportable.toNapiValue (env);
1261         auto status = napi_set_named_property (env, exports, fieldName, toExp);
1262       }
1263     }
1264     return exports;
1265   }
1266 
1267   // From the C macros that register the module.
1268 
1269   extern (C) static __gshared napi_module _module = {
1270     1  // nm_version
1271     , 0 // nm_flags
1272     , __FILE__.ptr
1273     , &.exportToJs
1274     , "NODE_GYP_MODULE_NAME"
1275     , null
1276   };
1277   
1278   version (Windows) { version (DigitalMars) {
1279     void main () {} // Dunno why it's needed but whatever
1280   }}
1281 
1282   extern (C) pragma (crt_constructor) export __gshared void _register_NAPI_MODULE_NAME () {
1283     napi_module_register (&_module);
1284   }
1285 }